#!/usr/env/bin python
"""
:mod:`pynchon.core.slice`
=========================

Provides array objects to store N-dimensional slices (tuples of slices).
"""

import numpy as np
from operator import mul
from copy import copy
from itertools import izip
from entity import EntityHierarchy, Entity_maker, Entity_sequencer, Entity_lohi
from pynchon.core.object import Object, ObjectVector, ObjectArray, ObjectCube, \
                                ObjectHyperCube

class Slice(Object):
    """
    Slice interface class. This class provides methods specific to 
    N-dimensional slice i.e. N-tuples of slice objects.
    """
    rank = 0
    fval = np.array(slice(0,0,0), dtype='object')

    @classmethod
    def slice2sss(cls, slice, len):
        """
        Transforms a slice object (or tuple of slice objects) into a start,
        stop, step tuple (or tuple of start, stop, step tuples). 
        """
        try:
            #slice is a slice object and len is an integer
            return slice.indices(len)
        except ValueError:
            # got dummy slice
            return None
        except AttributeError:
            # slice and len are tuples
            try:
                if slice.__len__() != len.__len__():
                    raise ValueError('The length of slice and len is not equal.')
            except AttributeError:
                raise ValueError('Both slice and length should be iterables.')    
            
            sss = []
            for slice_, len_ in zip(slice, len):
                sss.append(slice_.indices(len_))
            return tuple(sss)
        
    @classmethod
    def sss2indices(cls, sss):
        """
        Transforms a start, stop step tuple into a set of index integers.
        """
        try:
            return set(range(*sss))
        except TypeError:
            indices = []
            for sss_ in sss:
                indices.append(set(range(*sss_)))
            return tuple(indices)
           
    def sss(self, len =None):
        """
        Returns an array of (start, stop, step) tuples, assuming a sequence of
        a given length. A tuple is returned  for each ``slice`` object. 
        Dummy slices are transformed into ``None`` s. 
        """
        if self.rank == 0:
            slices = self.item()
            result = np.ndarray(shape=(1,), dtype='object')
            result[0] = self.slice2sss(slices, (len or self.metadata['len']))
        else:  
            # need array to store results
            result = np.ndarray(shape=(reduce(mul, self.shape),), dtype='object')
            for i, slices in enumerate(self.flat):
                result[i] = self.slice2sss(slices, (len or self.metadata['len']))
        result.shape = self.shape
        return result
    
    def indices(self, len =None):
        """
        Returns an array of sets of indices assuming a sequence of a given 
        length. A tuple of sets is returned for each ``slice`` object. Dummy 
        slices are transformed into ``None`` s.
        """
        sss = self.sss(len or self.metadata['len'])
        if self.rank == 0:
            indices = self.sss2indices(sss.item())
            result = np.ndarray(shape=(1,), dtype='object')
            result[0] = indices
        else:
            result = np.ndarray(shape=(reduce(mul, self.shape),), \
                                dtype='object')
            for i, sss_ in enumerate(sss.flat):
                if sss_ is not None:
                    result[i] = self.sss2indices(sss_)
                else:
                    result[i] = None
        result.shape = self.shape
        return result

class SliceVector(ObjectVector, Slice):
    """
    SliceVector interface class. This class provides methods specific to
    vectors of N-dimensional Slices e.g. 0D - [slice1, slice2, ... , sliceN],
    1D - [(slice1,), (slice2,), ... (sliceN,)], 2D - [(slice1a, slice1b), 
    (slice2a, slice2b), ... , (slice3a, slice3b)].
    """
    rank = 1
    
    def reduce_flat(self, operator, len=None):
        """
        Apply a set operator on the flattened indices array. The operator is
        applied on the slices in the order of the flat array.
        
        Arguments:
        
            * operator (string) - name of a ``set`` method, applied to reduce 
              the sets of slices into a single set.
            * len (int) - Assumed length of the sliced data.
            
        Returns a set of indices.
        """
        indices = self.indices(len or self.metadata['len'])
        indices_reduced = None
        for indices_ in indices.flat:
            if indices_reduced is not None:
                indices_reduced = getattr(indices_reduced, operator)(indices_)
            else:
                indices_reduced = indices_
        return indices_reduced
    
    def reduce_recursive(self, operators, len=None):
        """
        Apply multiple set operators along dimensions of the indices array. Each 
        dimension of the array can be reduced using a different operator. The 
        operators are applied recursively i.e. the operator reducing the last
        dimension is applied first.
        
        Arguments:
        
            * operators (tuple) - A tuple of operators one for each dimension.
              in the same order as the dimensions of the "shape" attribute.
            * len (int) - Assumed length of the sliced data.
            
        Returns a set of indices.
        """
        # get operator for current level
        operators = list(operators)
        operator = operators.pop(0)
        # get recursed class
        lo_clss, hi_clss = Entity_lohi(self)
        lo_cls = lo_clss[0]
        #
        indices_reduced = None
        for i in xrange(self.__len__()):
            # iterate over the first dimension
            # get array, view as Slice class, reshape
            lo_slice = self[i:i+1].view(lo_cls)
            lo_slice.shape = self.shape[1:]
            if lo_slice.rank != 0:
                indices_ = lo_slice.reduce_recursive(operators[:], \
                                       len or self.metadata['len']) # copy
            else:
                indices_ = lo_slice.indices(len or self.metadata['len']).item()
            if indices_reduced is not None:
                indices_reduced = getattr(indices_reduced, operator)(indices_)
            else:
                indices_reduced = indices_   
        return indices_reduced
            
            

class SliceArray(ObjectArray, SliceVector):
    """
    SliceArray interface class. This class provides methods specific to
    arrays of N-dimensional slice objects.
    """
    rank = 2


class SliceCube(ObjectCube, SliceArray):
    """
    SliceCube interface class. This class provides methods specific to
    cubes (3D) of N-dimensional slice objects.
    """
    rank = 3


class SliceHyperCube(ObjectHyperCube, SliceCube):
    """
    SliceHyperCube interface class. This class provides methods specific to
    cubes (4D) of N-dimensional slice objects.
    """
    rank = 4


# create classes
classes = Entity_maker(base_classes=(Slice, SliceVector, SliceArray, \
                                       SliceCube, SliceHyperCube),
                        dims=(0, 1, 2, 3, 4),
                        types=('object',)
                       )
# add to globals
for cls in classes:
    globals()[cls.__name__] = cls


#EOF